Search K
Appearance
Appearance
讲完前面建立连接、断开连接的过程,整个 TCP 协议的 11 种状态都出现了。TCP 之所以复杂,是因为它是一个有状态的协议。如果这个时候祭出下面的 TCP 状态变化图,估计大多数人都会懵圈,不要慌,我们会把上面的状态一一解释清楚。

上面这个图是网络上有人用 Latex 画出来了,很赞。不过有一处小错误,我修改了一下,如果感兴趣的话可以从我的 github 上进行下载,链接:tcp-state-machine.tex,在 overleaf 的网站可以进行实时预览。
这个状态是一个「假想」的状态,是 TCP 连接还未开始建立连接或者连接已经彻底释放的状态。因此 CLOSED 状态也无法通过 netstat 或者 lsof 等工具看到。
从图中可以看到,从 CLOSE 状态转换为其它状态有两种可能:主动打开(Active Open)和被动打开(Passive Open)
LISTEN 状态,这种被称为「被动打开」SYN 包准备三次握手,被称为「主动打开(Active Open)」一端(通常是服务端)调用 bind、listen 系统调用监听特定端口时进入到 LISTEN 状态,等待客户端发送 SYN 报文三次握手建立连接。
在 Java 中只用一行代码就可以构造一个 listen 状态的 socket。
ServerSocket serverSocket = new ServerSocket(9999);ServerSocket 的构造器函数最终调用了 bind、listen,接下来就可以调用 accept 接收客户端连接请求了。
使用 netstat 进行查看
netstat -tnpa | grep -i 9999
tcp6 0 0 :::9999 :::* LISTEN 20096/java处于 LISTEN 状态的连接收到 SYN 包以后会发送 SYN+ACK 给对端,同时进入 SYN-RCVD 阶段
客户端发送 SYN 报文等待 ACK 的过程进入 SYN-SENT 状态。同时会开启一个定时器,如果超时还没有收到 ACK 会重发 SYN。
使用 packetdrill 可以非常快速的构造一个处于 SYN-SENT 状态的连接,完整的代码见:syn_sent.pkt
+0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 connect(3, ..., ...) = -1运行上面的脚本,然后使用 netstat 命令查看连接状态 l
netstat -atnp | grep -i 8080
tcp 0 1 192.168.46.26:42678 192.0.2.1:8080 SYN_SENT 3897/packetdrill服务端收到 SYN 报文以后会回复 SYN+ACK,然后等待对端 ACK 的时候进入 SYN-RCVD,完整的代码见:state_syn_rcvd.pkt
+0 < S 0:0(0) win 65535 <mss 100>
+0 > S. 0:0(0) ack 1 <...>
// 故意注释掉下面这一行
// +.1 < . 1:1(0) ack 1 win 65535SYN-SENT 或者 SYN-RCVD 状态的连接收到对端确认 ACK 以后进入 ESTABLISHED 状态,连接建立成功。
把上面例子中脚本的注释取消掉,三次握手成功就会进入 ESTABLISHED 状态。
从图中可以看到 ESTABLISHED 状态的连接有两种可能的状态转换方式:
FIN-WAIT-1 状态FIN 包以后会回复 ACK,同时自己进入 CLOSE-WAIT 状态主动关闭的一方发送了 FIN 包,等待对端回复 ACK 时进入 FIN-WAIT-1 状态。
模拟的 packetdrill 脚本见:state_fin_wait_1.pkt
+0 < S 0:0(0) win 65535 <mss 100>
+0 > S. 0:0(0) ack 1 <...>
.1 < . 1:1(0) ack 1 win 65535
+.1 accept(3, ..., ...) = 4
+.1 close(4) = 0执行上的脚本,使用 netstat 就可以看到 FIN_WAIT1 状态的连接了
netstat -tnpa | grep 8080
tcp 0 0 192.168.73.207:8080 0.0.0.0:* LISTEN -
tcp 0 1 192.168.73.207:8080 192.0.2.1:52859 FIN_WAIT1 -FIN_WAIT1 状态的切换如下几种情况
ACK 以后,FIN-WAIT-1 状态会转换到 FIN-WAIT-2 状态FIN 以后,会回复对端 ACK,FIN-WAIT-1 状态会转换到 CLOSING 状态FIN+ACK 以后,会回复对端 ACK,FIN-WAIT-1 状态会转换到 TIME_WAIT 状态,跳过了 FIN-WAIT-2 状态处于 FIN-WAIT-1 状态的连接收到 ACK 确认包以后进入 FIN-WAIT-2 状态,这个时候主动关闭方的 FIN 包已经被对方确认,等待被动关闭方发送 FIN 包。
模拟的脚本见:state_fin_wait_2.pkt,核心代码如下
+0 < S 0:0(0) win 65535 <mss 100>
+0 > S. 0:0(0) ack 1 <...>
.1 < . 1:1(0) ack 1 win 65535
+.1 accept(3, ..., ...) = 4
+.1 close(4) = 0
+.1 < . 1:1(0) ack 2 win 257执行上的脚本,使用 netstat 就可以看到 FIN_WAIT2 状态的连接了
netstat -tnpa | grep 8080
tcp 0 0 192.168.81.69:8080 0.0.0.0:* LISTEN -
tcp 0 0 192.168.81.69:8080 192.0.2.1:34131 FIN_WAIT2 -当收到对端的 FIN 包以后,主动关闭方进入 TIME_WAIT 状态
当有一方想关闭连接的时候,调用 close 等系统调用关闭 TCP 连接会发送 FIN 包给对端,这个被动关闭方,收到 FIN 包以后进入 CLOSE-WAIT 状态。
完整的代码见:state_close_wait.pkt
+.1 < F. 1:1(0) win 65535 <mss 100>
+0 > . 1:1(0) ack 2 <...>执行上的脚本,使用 netstat 就可以看到 CLOSE_WAIT 状态的连接了
sudo netstat -tnpa | grep -i 8080
tcp 0 0 192.168.168.15:8080 0.0.0.0:* LISTEN 15818/packetdrill
tcp 1 0 192.168.168.15:8080 192.0.2.1:44948 CLOSE_WAIT 15818/packetdrill当被动关闭方有数据要发送给对端的时候,可以继续发送数据。当没有数据发送给对方时,也会调用 close 等系统调用关闭 TCP 连接,发送 FIN 包给主动关闭的一方,同时进入 LAST-ACK 状态
TIME-WAIT 可能是所有状态中面试问的最频繁的一种状态了。这个状态是收到了被动关闭方的 FIN 包,发送确认 ACK 给对端,开启 2MSL 定时器,定时器到期时进入 CLOSED 状态,连接释放。TIME-WAIT 会有专门的文章介绍。
完整的代码见:state_time_wait.pkt
// 服务端主动断开连接
+.1 close(4) = 0
+0 > F. 1:1(0) ack 1 <...>
// 向协议栈注入 ACK 包,模拟客户端发送了 ACK
+.1 < . 1:1(0) ack 2 win 257
// 向协议栈注入 FIN,模拟服务端收到了 FIN
+.1 < F. 1:1(0) win 65535 <mss 100>
+0 `sleep 1000000`执行上的脚本,使用 netstat 就可以看到 TIME-WAIT 状态的连接了
netstat -tnpa | grep -i 8080
tcp 0 0 192.168.210.245:8080 0.0.0.0:* LISTEN 6297/packetdrill
tcp 0 0 192.168.210.245:8080 192.0.2.1:40091 TIME_WAIT -LAST-ACK 顾名思义等待最后的 ACK。是被动关闭的一方,发送 FIN 包给对端等待 ACK 确认时的状态。
完整的模拟代码见:state_last_ack.pkt
// 向协议栈注入 FIN 包,模拟客户端发送了 FIN,主动关闭连接
+.1 < F. 1:1(0) win 65535 <mss 100>
// 预期协议栈会发出 ACK
+0 > . 1:1(0) ack 2 <...>
+.1 close(4) = 0
// 预期服务端会发出 FIN
+0 > F. 1:1(0) ack 2 <...>
sudo netstat -lnpa | grep 8080 1 ↵
tcp 0 0 192.168.190.26:8080 0.0.0.0:* LISTEN 6163/packetdrill
tcp 1 1 192.168.190.26:8080 192.0.2.1:36054 LAST_ACK当收到 ACK 以后,进入 CLOSED 状态,连接释放。
CLOSING 状态在「同时关闭」的情况下出现。这里的同时关闭中的「同时」其实并不是时间意义上的同时,而是指的是在发送 FIN 包还未收到确认之前,收到了对端的 FIN 的情况。
我们用一个简单的脚本来模拟 CLOSING 状态。完整的代码见 state-closing.pkt
+0.100 write(4, ..., 1000) = 1000
+0 > P. 1:1001(1000) ack 1 <...>
+0.01 < . 1:1(0) ack 1001 win 257
+.1 close(4) = 0
+0 > F. 1001:1001(0) ack 1 <...>
+.1 < F. 1:1(0) ack 1001 win 257
+0 > . 1002:1002(0) ack 2 <...>运行 packetdrill 执行上面的脚本,同时开启抓包。
使用 netstat 查看当前的连接状态就可以看到 CLOSING 状态了。
netstat -lnpa | grep -i 8080
tcp 0 0 192.168.60.204:8080 0.0.0.0:* LISTEN -
tcp 1 1 192.168.60.204:8080 192.0.2.1:55456 CLOSING -使用 wireshark 查看如下图所示,完整的抓包文件可以从 github 下载:state-closing.pcap

整个过程如下图所示

到这里,TCP 的 11 种状态就介绍完了,我为了你准备了几道试题,看下自己的掌握的情况吧。
下列 TCP 连接建立过程描述正确的是:
2*MSL 时间后就会进入 SYN_SENT 状态2*MSL 时间后会直接关闭连接TCP 连接关闭,可能有经历哪几种状态: